/**
 * \brief 使用ffmpeg将yuv编码成hevc或h264
 * \details 视频录制完成后在debug-编译目录下会有test.hevc或test.h264
 *          ffmpeg播放命令：
 *          ffplay test.hevc
 * \note 第一次编译运行会报错！注意事项：
 * 先点击“Debug”运行一次，自动创建出可执行文件输出文件夹
 * 此时“3 应用程序输出”窗口最后一行只是显示了红色的异常信息“程序异常结束。”，
 * 则表示没有把
 * ../006_AudioVideo_Codec_ffmpeg/ffmpeg-4.3.1-win64-shared/bin/
 * 下的*.dll动态库拷贝到
 * ../build-Embeded_Player-Desktop_Qt_6_2_3_MinGW_64_bit-Debug/debug/
 * 下的可执行文件目录中，拷贝完成后再点击“Debug”运行，软件才能正常工作。
 * \note .lib文件是编译时需要的，里面指定了一些动态库的链接符号，
 *       真正运行时依靠的是.dll动态库
 *
 * \note libavcodec默认通过id查找编解码器的方式都只能找到软解码器，通过名字
 *       名字查找的方式才能找到硬解码器，可以通过ffmpeg -codecs命令查看软硬件解码器的名字
 *
 * \details 参考网址（该代码拷贝过来后可以一次性编译运行通过）：
 *          [FFmpeg —— 13.示例程序（七）：视频编码器（YUV编码为H265）](https://www.dandelioncloud.cn/article/details/1441257592063643649)
 * \author 将狼才鲸
 * \date 2022-04-02
 */

/**** 头文件 ****/
#include "module.h"
#include <string.h> // memcpy
#include <stdio.h>  // printf fflush

#include "libavformat/avformat.h" // file、http等读写，mp4、mkv等容器封装解封装
#include "libavcodec/avcodec.h"   // 音频、视频、字幕编解码
#include "libavutil/imgutils.h"   // 获取编码时循环输入的一帧数据大小
#include "libswscale/swscale.h"   // NV12转YUV420

/**** 宏定义 ****/
#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define pr_info printf
#define pr_err printf
#define MDATA_LEN (2 * 1024 * 1024) // for 1280 * 720 max

/**** 全局变量（供别的模块调用） ****/
int hevcmdatalen = 0;   /* 既作为长度，又作为消费者生产者的锁 */

/**** 模块内私有变量 ****/
static int tick;    /* 当前帧的时间戳，单位ms */
static int tick_start_value;
static int tick_start_flag = 0;
/* 本模块缓存数据用（以后会修改软件架构，替换成缓存队列） */
static unsigned char mdata[MDATA_LEN];  /* 帧数据 */
static unsigned char yuv420_mdata[MDATA_LEN];
static unsigned char packet_data[MDATA_LEN];    /* 已编码后的包数据 */
static int packet_data_len;
static struct module *target = NULL;    /* 目的模块（向它发送数据） */
static int video_type;  /* 1: h264, 2: hevc */

/* ffmpeg libavcodec解码需要的一些变量 */
static AVFormatContext *pFormatCtx = NULL; // 输入输出容器共用的封装解封装主数据
static AVOutputFormat *pOutFmt = NULL;     // 输出容器封装（不用单独申请）
static AVStream *pOutStream = NULL;        // 输出视频流，里面会用到编码器
static AVCodecContext *pCodecCtx = NULL;   // 输出编码器要用到的数据主结构体
static AVCodec *pCodec = NULL;             // 输出编码器
static AVFrame *pFrame = NULL;             // 编码时的每个输入帧
static AVPacket *pPacket = NULL;           // 编码后的每个已编码帧
static struct SwsContext *m_pSwsCtx = NULL;// NV12转YUV420P
static int frameIndex = 0, packetIndex = 0;// 输入输出帧号
static int ySize = 0;                      // 一帧yuv文件大小

//#define USE_YUV420
#define USE_NV12 // 我的摄像头输入的是NV12 1280 * 720格式的YUV

//#define DIRECT_SAVEAS_FILE // 用于测试编码时直接输出的完整文件是否能正常播放
#ifdef DIRECT_SAVEAS_FILE
static const char *outFilename;
#endif

/**** 模块内私有函数 ****/
static int codec_start(void)
{
    int ret = 0;
    char *decoder_name;
    //TODO: 需要将所有变量重新清零一遍，以便能够重复start

    /* 1. 注册所有输入输出设备，并且初始化avdevice库，当前我没有用到输入输出设备 */
    /** libavdevice的接口函数不多，只有十多个，比较简单 */
    //avdevice_register_all(); // libavdevice: avdevice.h

    /**
     * ffmpeg上下文context是一个很玄妙的概念，我用语言说不清，要好好读源码体会；
     * 基本上不同函数接口之间的数据传递就靠各级模块的context，是主要的数据结构体
     */
    /**
     * libavformat主要用于IO文件或者网络流的读写、mp4/mkv等面向用户使用的
     * 这类文件的封装解封装（也称为复用和解复用），将容器里的音频和视频码流
     * 单独提取出来。
     * libavformat里面的结构体的成员都超多，多写代码才能知道怎么用
     * 单纯的音视频流，如mp3、aac、h264、hevc也要当成一种特殊的容器封装来进行处理
     *
     * libavformat里面不能不使用封装而直接申请出流AVStream，只处理流也要先将流
     * 当为容器中特殊的一类来处理
     */

    /* 2. 分配输入输出容器封装上下文（结构体），同时也初始化输出容器封装 */
    // 也可以不使用AVFormatContext，仅仅只使用AVCodec
    /**
     * \brief 分配输出容器封装上下文（结构体）
     * \details 这个函数可以按输出文件名后缀查找、按输出封装结构体查找、
     *          也可以按输出容器名查找;
     *          分配成功后里面的AVOutputFormat输出格式结构体也同时被分配了
     * ffmpeg -formats 命令能查看所有支持的输入输出封装格式
     * 视频流类：
     * DE h264            raw H.264 video
     * DE hevc            raw HEVC video
     * DE m4v             raw MPEG-4 video
     *  E mpeg2video      raw MPEG-2 video
     * 容器封装类：
     *  E matroska        Matroska(mkv)
     * D  matroska,webm   Matroska / WebM(mkv)
     *  E mov             QuickTime / MOV(mp4)
     * D  mov,mp4,m4a,3gp,3g2,mj2 QuickTime / MOV(mp4)
     * DE mpegts          MPEG-TS (MPEG-2 Transport Stream)
     */
    if (video_type == 1) {
        decoder_name = "h264";
#ifdef DIRECT_SAVEAS_FILE
        outFilename = "test2.h264";
#endif
    } else { // video_type == 2
        decoder_name = "hevc";
#ifdef DIRECT_SAVEAS_FILE
        outFilename = "test2.hevc";
#endif
    }

    avformat_alloc_output_context2(&pFormatCtx, NULL, decoder_name, NULL);
    /* 另一种方法：直接分配空的输入输出容器共用的封装上下文（主结构体） */
    //pFormatCtx = avformat_alloc_context(); // libavformat: avformat.h
    //if (!pFormatCtx) {
    //    pr_err("avformat alloc memary fail\n");
    //    return -EPERM;
    //}
    // 找到输入和输出容器格式，再赋值
    //av_register_input_format(pInFmt);
    //pInFmt = av_find_input_format("你的输入格式名称");
    //pFormatCtx->iformat = pInFmt;

#ifdef DIRECT_SAVEAS_FILE
    // 打开输出文件
    if (avio_open(&pFormatCtx->pb, outFilename, AVIO_FLAG_WRITE) < 0)
    {
        pr_err("can't open output file\n");
        ret = -EPERM;
        goto err1;
    }
#endif

    /*
     * 从已分配的输入输出容器封装共用的封装上下文中赋值给输出容器封装结构体,
     * 里面会有编码器的id
     *
     * 即使你只是需要单纯的使用输出的音频流或者视频流，也需要先将AVStream放在
     * 特殊的输出容器封装中进行处理，不能只处理视频流而不处理封装，
     * 除非你只用AVCodec编码器和每个帧AVFrame，AVPacket
     */
    pOutFmt = pFormatCtx->oformat;
    if (!pOutFmt) {
        pr_err("get hevc output format error\n");
        ret = -EPERM;
        goto err1;
    }

    /* 3. 查找编码器 */
    /**
     * \brief 使用名字查找时才能找到硬解码器
     * \details 查看所有软硬件解码器名称：ffmpeg -codecs
     *  DEV.LS h264    H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (decoders: h264 h264_qsv h264_cuvid ) (encoders: libx264 libx264rgb h264_amf h264_nvenc h264_qsv nvenc nvenc_h264 )
     *  DEV.L. hevc    H.265 / HEVC (High Efficiency Video Coding) (decoders: hevc hevc_qsv hevc_cuvid ) (encoders: libx265 nvenc_hevc hevc_amf hevc_nvenc hevc_qsv )
     *          其中hevc_nvenc hevc_qsv h264_nvenc h264_qsv是硬件解码器
     */
    ///当前使用硬件编码器时运行崩溃了，在哪里还有个bug，当前没准备去查
    //pCodec = avcodec_find_encoder_by_name("hevc_qsv");
    // 使用id查找时只支持软解码
    pCodec = avcodec_find_encoder(pOutFmt->video_codec);
    if (!pCodec)
    {
        printf("can't find encoder\n");
        ret = -EPERM;
        goto err1;
    }
    pr_info("pCodec.name = %s\n", pCodec->name);

    /* 4. 配置编码输出参数 */
    // 若是h264/hevc编码器，要设置一些参数
    AVDictionary *param = NULL;
    if (pOutFmt->video_codec == AV_CODEC_ID_H264)
    {
        av_dict_set(&param, "preset", "slow", 0);
        av_dict_set(&param, "tune", "zerolatency", 0);
    }
    // hevc(H.265)
    if(pOutFmt->video_codec == AV_CODEC_ID_H265){
        //av_dict_set(&param, "x265-params", "qp=20", 0);
        av_dict_set(&param, "preset", "ultrafast", 0);
        av_dict_set(&param, "tune", "zero-latency", 0);
    }

    /* 5. 创建输出码流
     * 另一种方法：也可以先不创建输出流，输出流在后面再申请，先直接使用解码器，
     * 在输出文件已经打开过的情况下
     * avio_open(&pFormatCtx->pb, outFilename, AVIO_FLAG_WRITE);
     * 将pCodec使用
     * avcodec_find_encoder(pFormatCtx->oformat->audio_codec);
     * 直接创建
     */
    pOutStream = avformat_new_stream(pFormatCtx, NULL);
    if (!pOutStream)
    {
        pr_err("can't allocate new stream\n");
        ret = -EPERM;
        goto err1;
    }

    /* 6. 配置编码器 */
    pCodecCtx = pOutStream->codec;
    /* 默认的b帧数量是0，需要将其改为需要的值，不改的话只会编码I帧和P帧 */
    printf("codec: default max b frame num: %d, set to 3\n", pCodecCtx->max_b_frames);
    pCodecCtx->max_b_frames = 3;
    // 设置编码器上下文参数
    pCodecCtx->codec_id = pOutFmt->video_codec;
    pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;    /* 好像编码器不支持AV_PIX_FMT_NV21??? */
    // 视频的宽度和高度：确保等于输入文件的宽度和高度
    pCodecCtx->width = 1280; //TODO: 改成按参数自动赋值的
    pCodecCtx->height = 720;
    // 设置帧率25fps，也就是3个时间戳里的tbr，另外两个是tbn, tbc
    pCodecCtx->time_base.num = 1;
    pCodecCtx->time_base.den = 25;
    // 设置码率
    pCodecCtx->bit_rate = 400000;
    // 设置GOP
    pCodecCtx->gop_size = 250;
    // 设置量化参数
    pCodecCtx->qmin = 10;
    pCodecCtx->qmax = 51;

    /* 7. 打开编码器 */
    if (avcodec_open2(pCodecCtx, pCodec, &param) < 0)
    {
        printf("can't open encoder\n");
        ret = -EPERM;
        goto err1;
    }

    /* 8.写入文件头信息（像输出格式是mp4、mkv等容器就一定要写头，里面包含了索引信息，h264、hevc是流就没有头） */
#ifdef DIRECT_SAVEAS_FILE
    avformat_write_header(pFormatCtx, NULL);
#endif

    /* 9. 为循环编码YUV文件为h264/hevc做准备 */
    /* (1) 获取一帧输入的大小 */
    int bufSize = av_image_get_buffer_size(pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, 1);
    ySize = pCodecCtx->width * pCodecCtx->height;

    /* (2) 默认帧参数填充（格式、宽度和高度） */
    pFrame = av_frame_alloc();
    pFrame->format = pCodecCtx->pix_fmt;
    pFrame->width = pCodecCtx->width;
    pFrame->height = pCodecCtx->height;

    av_image_fill_arrays(pFrame->data,
            pFrame->linesize,
            mdata,
            pCodecCtx->pix_fmt,
            pCodecCtx->width,
            pCodecCtx->height,
            1);
    /* (3) 开辟编码后的一个包（一帧已编码数据） */
    pPacket = (AVPacket *)av_malloc(bufSize); // 后续记得释放

#ifdef USE_NV12
    // 注册NV12转YUV420P的句柄，当前编码器只支持YUV420P输入
    m_pSwsCtx = sws_getContext(pCodecCtx->width,
           pCodecCtx->height,
           AV_PIX_FMT_NV12,
           pCodecCtx->width,
           pCodecCtx->height,
           AV_PIX_FMT_YUV420P, SWS_FAST_BILINEAR, NULL, NULL, NULL);
#endif

    return ret;

err1:
    if (pFormatCtx)
        avformat_free_context(pFormatCtx);

    return ret;
}

extern int mp4vmdatalen;

/* 编码一帧数据，获取到一帧编码包前会阻塞 */
static int codec_parse(void)
{
    int ret = 0;
    char *indata = mdata;
    char *outdata = yuv420_mdata;

#ifdef USE_NV12
    /* NV12转YUV420 */
    //[使用ffmpeg编码原始nv12帧](http://cn.voidcc.com/question/p-rajvdmfe-dw.html)
    //[全网首发：FFMpeg使用NVIDIA DECODER，解码后的数据是NV12，不是YUV420P](https://blog.51cto.com/u_13161667/3306044)
    int nv12_linesize[3];
    int yuv420_linesize[3];
    yuv420_linesize[0] = pCodecCtx->width;     // YUV各个分量的长度
    yuv420_linesize[1] = pCodecCtx->width / 2;
    yuv420_linesize[2] = pCodecCtx->width / 2;
    nv12_linesize[0] = pCodecCtx->width;
    nv12_linesize[1] = pCodecCtx->width;
    nv12_linesize[2] = 0;
    unsigned char *inaddr[3] = {indata, indata + ySize, 0};
    unsigned char *outaddr[3] = {outdata, outdata + ySize, outdata + ySize + ySize / 4};
    ret = sws_scale(m_pSwsCtx, inaddr, nv12_linesize, 0, pCodecCtx->height,
                    outaddr, yuv420_linesize);
    indata = yuv420_mdata;
#endif

    /* (4) 循环编码 */
    /* 将缓冲区数据转换成AVFrame需要的类型：
       YUV420是planar平面存储的模式（另一种是packed交替存储如NV12），
       frame传入的数据最多支持8个平面，每个平面的数据指针要分开 */
    pFrame->data[0] = indata;                     // Y值
    pFrame->data[1] = indata + ySize;             // U值
    pFrame->data[2] = indata + ySize + ySize / 4; // V值

    // 使用12800为时间基，40ms时长对应512的pts值
    // 1s    1000ms Nms(40ms/25fps)
    // 12800 12800  Xpts ==> X(pts) = N * 12800 / 1000
#if 1 /* 测试用，让每个视频帧间隔一样 */
    pFrame->pts = (frameIndex * 80 * 12800) / 1000;
#else
    //TODO: 需要在最开始获取摄像头帧时从系统模块中获取时间，后续手动获取的时间因为OS中的进程调度，总是不准
    pFrame->pts = ((tick - tick_start_value) * 12800) / 1000;   /* 时间基由1000转成12800 */
#endif
    /* 不像下载的电影或视频，每帧pts相差都是一个固定值，录制的视频pts是一个波动值 */

//    printf("\n[frame]frameId: %d\t", frameIndex);
//    printf("width: %d\t", pFrame->width);
//    printf("height: %d\t", pFrame->height);
//    printf("pts: %lld\t", pFrame->pts);

    /* 10.视频编码处理 */
    /* (1) 发送一帧视频像素数据 */
    if (avcodec_send_frame(pCodecCtx, pFrame) < 0) {
        pr_err("failed send to encoder\n");
        return -EPERM;
    } else {
        frameIndex++;
    }

    /* (2) 接收一帧视频压缩数据格式（像素数据编码而来） */
    // 此函数内会阻塞住，直到获取到数据
    if (avcodec_receive_packet(pCodecCtx, pPacket) >= 0) {
        // 编码成功
        /* 11. 将编码的一帧返回 */
        pPacket->stream_index = pOutStream->index;

//        printf("[packet]pts: %lld\t", pPacket->pts);
//        printf("dts: %lld\t", pPacket->dts);
//        printf("flags(keyFrame): %d\t", pPacket->flags);
//        printf("size: %d\t", pPacket->size);

        /* 出来的第一个帧是关键帧，pts是0，但是dts是-1024，
           之后pts和dts一一对应，dts从-1024开始以512递增 */

        // 将已编码包发送给下一个模块进行处理
//        while (mp4vmdatalen) {}
        if (target) {
            packet_data_len = 0;
            memcpy(packet_data + packet_data_len, &pPacket->pts, sizeof(int64_t));
            packet_data_len += sizeof(int64_t);
            memcpy(packet_data + packet_data_len, &pPacket->dts, sizeof(int64_t));
            packet_data_len += sizeof(int64_t);
            memcpy(packet_data + packet_data_len, &pPacket->flags, sizeof(int));
            packet_data_len += sizeof(int);
            memcpy(packet_data + packet_data_len, pPacket->data, pPacket->size);
            packet_data_len += pPacket->size;
            target->putdata(packet_data, packet_data_len, 1);
        }

        packetIndex++;

#ifdef DIRECT_SAVEAS_FILE
        av_write_frame(pFormatCtx, pPacket); // 如果写入到输出文件
#endif

        //pr_info("succeed to write packet: %d\tsize:%d\n", packetIndex++, pPacket->size);
    } else {
        return -ENOENT;
    }

    av_packet_unref(pPacket);

    return 0;
}

static int flush_encoder(AVFormatContext *fmtCtx, unsigned int streamIndex)
{
    int ret;
    int got_frame;
    AVPacket enc_pkt;
    if (!(fmtCtx->streams[streamIndex]->codec->codec->capabilities & AV_CODEC_CAP_DELAY))
    {
        return 0;
    }
    while (1)
    {
        enc_pkt.data = NULL;
        enc_pkt.size = 0;
        av_init_packet(&enc_pkt);
        ret = avcodec_encode_video2 (fmtCtx->streams[streamIndex]->codec, &enc_pkt,
            NULL, &got_frame);
        av_frame_free(NULL);
        if (ret < 0)
            break;
        if (!got_frame){
            ret=0;
            break;
        }

        /* 只有开启B帧后，退出编码时才会进入这个后处理函数 */

        printf("Flush Encoder: Succeed to encode 1 frame!\tsize:%5d\n",
               enc_pkt.size);
        /* mux encoded frame */
#ifdef DIRECT_SAVEAS_FILE
        av_write_frame(fmtCtx, &enc_pkt);
#endif

        if (target) {
            packet_data_len = 0;
            memcpy(packet_data + packet_data_len, &enc_pkt.pts, sizeof(int64_t));
            packet_data_len += sizeof(int64_t);
            memcpy(packet_data + packet_data_len, &enc_pkt.dts, sizeof(int64_t));
            packet_data_len += sizeof(int64_t);
            memcpy(packet_data + packet_data_len, &enc_pkt.flags, sizeof(int));
            packet_data_len += sizeof(int);
            memcpy(packet_data + packet_data_len, enc_pkt.data, enc_pkt.size);
            packet_data_len += enc_pkt.size;
            target->putdata(packet_data, packet_data_len, 1);
        }
    }
    return ret;
}

static int codec_stop(void)
{
    printf("\n");

    // 写入剩余帧数据（可能没有）
    flush_encoder(pFormatCtx, 0);

    // 写入文件尾部信息
#ifdef DIRECT_SAVEAS_FILE
    av_write_trailer(pFormatCtx);
#endif

    // 释放内存
    sws_freeContext(m_pSwsCtx);
    avcodec_close(pCodecCtx);
    av_free(pFrame);
    av_packet_free(&pPacket);
    avio_close(pFormatCtx->pb);
    avformat_free_context(pFormatCtx);

    return 0;
}

/**** 模块成员函数（供别人通过函数指针调用） ****/
static int module_init(void)
{
    if (target)
        target->init();

    return 0;
}

static int module_start(void *param)
{
    start_param_t sparam;
    start_param_t *isparam = param;

    if (isparam == NULL)
        return -EPERM;

    hevcmdatalen = 0;

    if (target) {
        if (isparam->av_type == AVTYPE_H264) {
            sparam.file_name = "test.h264";
            sparam.av_type = AVTYPE_H264;
            video_type = 1;
        } else { //if (isparam->av_type == AVTYPE_HEVC)
            sparam.file_name = "test.hevc";
            sparam.av_type = AVTYPE_HEVC;
            video_type = 2;
        }
        target->start(&sparam);
    }

    codec_start();

    return 0;
}

static int module_run(void)
{
    int ret;

    if (target)
        target->run();

    if (!hevcmdatalen)  /* 如果没有数据，未获取到锁 */
        return 0;

    ret = codec_parse();

    if (ret != -EPERM)
        hevcmdatalen = 0; // 释放锁

    return 0;
}

static int module_stop(void)
{
    if (target)
        target->stop();

    codec_stop();

    return 0;
}

static int module_exit(void)
{
    if (target)
        target->exit();

    return 0;
}

static int module_settarget(void *mtarget)
{
    target = (struct module *)mtarget;

    return 0;
}

static int module_putdata(void *data, int len, int flag)
{
    if (hevcmdatalen)   /* 如果有数据，未获取到锁 */
        return 0;

    if (!data || len > MDATA_LEN)
        return 0;

    /* 数据前面是int类型的时间戳，后面才跟着帧数据 */
    memcpy(&tick, data, sizeof(int));
    if (!tick_start_flag) {
        /* 如果是录制后第一次获取视频帧 */
        tick_start_value = tick;
        tick_start_flag = 1;
    }
    memcpy(mdata, data + sizeof(int), len - sizeof(int));
    hevcmdatalen = len;

    return 0;
}

struct module g_module_videoenc = {
    "video encode", module_init,
    module_start, module_run,
    module_stop, module_exit,
    module_settarget, module_putdata
};
